Delphi LiveBindings Spelunking - Felix John COLIBRI. |
- abstract : analysis of the architecture of the Delphi LiveBindings : how the tBindingExpression compiles a String expression to build an environment referencing objects which can be evaluated to fill component
properties. Dump of the pseudo code and UML Class Diagram of the LiveBinding architecture
- key words : tBindingExpression, iScope, iValue, tNestedScope,
tBindingOutput, tCompiledBinding - Interface to Object conversion
- software used : Windows XP Home, Delphi XE2, update 1 installed
- hardware used : Pentium 2.800Mhz, 512 M memory, 140 G hard disc
- scope : Delphi XE2, Vcl and FireMonkey
- level : Delphi developer
- plan :
1 - Delphi LiveBindings Livebindings are the new data binding technique which allows us to link objects to each other using String expressions.
Delphi LiveBindings is available for both the Vcl and FireMonkey. For the Vcl, it allows us to bind tWinControls for which no db_xxx data sensitive controls were provided. And for Firemonkey it is at the heart of the database visual
management, since no db_xxx are available : linking any FireMonkey visual control to a Database using LiveBinding is possible.
In this article, we will explore the architecture of LiveBindings using the simplest possible binding.
But before starting the deep dive, let us stress that going this deep into the innards of LiveBindings is not at all required in order to implement LiveBindings in your applications. Daily use of LiveBindings is quite simple and straightforward.
However, knowing what's under the hood, especially given the current state of the documentation, could not hurt too much.
To make sure that readers casually looking at the article understand that
LiveBinding is easy, we will first present 3 tutorial examples : - binding a tColorPanel to to the color of a tRectangle
- displaying the CATEGORY field of the BIOLIFE (FISHFACTS) Database Table to a tEdit
- displaying the tPerson.FirstName in a tLabel.Caption using a simple tMemo.Lines expression
2 - LiveBindings Tutorial
2.1 - A tColorPanel changes the target tRectangle color For this first example we want to use a tColorPanel (a kind of 2D tTrackBar used to define color values) in order to change a tRectangle color. In this case
- the source (the origin) is the tColorPanel
- the target (what is changed by the LiveBinding) is the tRectangle color
In traditional Vcl Database binding, we use - a tDataSource as the source
- a db_xxx control as the target (the tWinControl)
and this explains the "source -> control" terminology. In order to link the tColorPanel changes to the color of the tRectangle, we
have to use a tBindExpression which has 4 parameters - for the source
- SourceComponent, here ColorPanel1
- SourceExpression, here ColorPanel1.Color
- for the target (the "control")
- ControlComponent, which is Rectangle1
- ControlExpression, in our case Rectangle1.Fill.Color
To build this Firemonkey example
| create a FireMonkey project with "File | New | FireMonkey HD Application" | |
drop a tColorPanel on the Form, name it source_color_panel | | drop a tRectangle on the Form, name it target_rectangle |
| make sure target_rectangle_ is selected | |
in the Object Inspector, select the LiveBinding property, Click on it and select "New LiveBinding ..." | | a "New LiveBinding is displayed |
| select tBindExpression and click on it | |
a BindingsList1 is automatically added to Form1, and a new tBindEpression is added as a child component of target_rectangle_, and automatically named BindExpressiontarget_rectangle_1 |
| in the top left corner, in the Structure TreeView1 click target_rectangle_, LiveBindings, BindExpressiontarget_rectangle_1 |
| BindExpressiontarget_rectangle_1 is displayed in the Object Inspector :
where you see - in red the BindExpressiontarget_rectangle_1 in the Structure TreeView
- in green the SourceComponent and SourceExpression properties
- in grey the ControlComponent (target_rectangle_) and ControlExpression
| | set the 4 LiveBinding parameters
- in SourceComponent, open the combobox and select source_color_panel_
- in SourceExpression type Color
- in ControlComponent, keep target_rectangle_ selected
- in ControlExpression, type Fill.Color
| | compile and run |
| the target_rectangle_ automatically takes the default source_color_panel green color: |
However any change in the source_color_panel is not automatically forwarded to the target_rectangle_. To improve this we must trigger the change from
source_color_panel_.OnChange event, and call BindingsList1.Notify. Here is the code:
Two possible initial mistakes - the confusion about the "source" and the "control". Both are "tWinControls".
Simply keep the database analogy in mind (the original information comes from the tDataSource and flows towards the tDbEdit control)
- to which component should the tBindExpression added. Database to the rescue
again: we link from the tDbEdit to the tDataSource. Therefore, from the target_rectangle_ back to the source_color_panel_. The reason in both case is the 1-n multiplicity: it is easier to specify on the target side which is
the only possible source (rather than having a list to specify all the possible targets)
2.2 - Linking a tEdit to a tDataSet In FireMonkey, there are no "data sensitive" controls, like the tDbEdit Vcl
control. In FireMonkey, LiveBindings are used to build database applications. And linking any control to the database is really simple. Here is how to link the CATEGORY field of the FISHFACT table to a FireMonkey tEdit.
First we setup a FireMonkey application with an open tDataSet : | create a FireMonkey project with "File | New | FireMonkey HD Application"
and save it in a folder | | from C:\Program Files\Common Files\CodeGear Shared\Data\
copy the FISHFACTS.CDS and paste it in the same folder as the .DPR. Actually, you can use any .CDS, or link the any database using dbExpress, or ADO. We simply chose the local file to avoid detailing the connection |
| drop a tClientDataSource on Form1 and - in FileName, type FISHFACTS.CDS
- toggle Active to True
Alternately just set up any tDataSet descendent, and open it | | drop a tDataSource component, and link DataSource1.DataSet to
ClientDataSet1 |
Now the LiveBindings part
That's it. To move to the next row in the DataBase, we can add a BindNavigator, and use LiveBinding to allow navigation: |
drop a BindNavigator on Form1 | | in the Object Inspector, select BindScope and select BindScope1 |
| compile, run and navigate ! |
2.3 - LiveBinding by code To understand the LiveBinding architecture, we will use the simplest
LiveBinding possible, which is the evaluation of a StringExpression. We will use - a tPerson Class with a FirstName property defined by
Type tperson =
Class(TObject) Private
FFirstName: String;
Published
Property Firstname : String read FFirstName write FFirstName;
End; | - the following source expression:
"nickname " + my_friend.FirstName | - the target is a tLabel.Caption
Here is the code:
| create a VCL application (Firemonkey would work in exactly the same manner) | | create the c_person Class |
| drop a tMemo, name it source_expression_memo_, and in Lines type the source expression |
| drop a tButton to perform the computation and name it evaluate_expression_: | |
drop a target tLabel control and name it target_label_ | | add System.Bindings.ExpressionDefaults and System.Bindings.Expression to
the Uses clause | | in the evaluate_expression OnClick, type the following code:
Var g_c_person: tPerson= Nil;
g_c_binding_expression: tBindingExpressionDefault= Nil;
Procedure TForm1.evaluate_expression_Click(Sender: TObject);
Begin g_c_person:= tPerson.Create;
g_c_person.Firstname:= 'Archi';
g_c_binding_expression:= tBindingExpressionDefault.Create;
With g_c_binding_expression Do Begin
Source:= source_expression_memo_.Lines.Text;
Compile([
tBindingAssociation.Create(g_c_person, 'my_friend')
]);
Outputs.Add(target_label_, 'Caption');
EvaluateOutputs; End;
End; // evaluate_expression_Click | Where - the g_c_binding_expression is the main component to compile the
expression
- The Source property will contain the String to be evaluated
- the Compile call receives a tBindingAssociation object which is used to link
- the g_c_person object
- an the "my_friend" string used in the source expression
- Outputs.Add specifies the target and the receiving property
- EvaluateOutputs uses the compiled expression to place the result int
the label's caption
| | compile and run | |
here is the result |
3 - LiveBindings Internals 3.1 - LiveBinding Architecture Our goal is to be able to draw the UML Class
Diagram of a simple LiveBinding example.
We used the simple expression evaluator presented above, and simple decomposed the code in several parts to be able to display the different elements involved
in LiveBinding. And this allowed us to draw the UML Class Diagram.
In the code used for analysis, we used the following parts : - two Classes
Type c_person =
Class(TObject) Private
FFirstName: String;
FLastName: String;
FAge: Integer;
Published
Function ToString: String; Override;
Property Firstname : String read FFirstName write FFirstName;
Property Lastname : String read FLastName write FLastName;
Property Age : Integer read FAge write FAge;
End; c_company=
Class(tObject) Private
fCompany: String;
Published
Property Company : String read FCompany write FCompany;
End;
Function c_person.ToString: String; Begin
Result:= FirstName+ ' '+ IntToStr(Age);
End; // ToString Procedure create_objects;
Begin g_c_person:= c_person.Create;
g_c_person.Firstname:= 'Archibald';
g_c_person.LastName:= 'LERICH';
g_c_person.Age:= 33;
g_c_company:= c_company.Create;
g_c_company.Company:= 'CRESUS Inc';
End; // create_objects | - two method to compile and to evaluate the expression:
Procedure create_and_compile(p_source: String);
Var l_c_person_binding_association: tBindingAssociation;
l_c_company_binding_association: tBindingAssociation;
l_c_amount_binding_association: tBindingAssociation; Begin
l_c_person_binding_association:= tBindingAssociation.Create(g_c_person, 'the_person');
l_c_company_binding_association:= tBindingAssociation.Create(g_c_company, 'the_company');
g_c_binding_expression:= c_my_binding_expression_default.Create;
With g_c_binding_expression Do Begin
Source:= p_source; Compile([
l_c_person_binding_association,
l_c_company_binding_association ]);
End; // with g_c_binding_expression
End; // create_and_compile
Procedure set_outputs_and_evaluate(p_c_output_label: tLabel);
Begin With g_c_binding_expression Do
Begin
Outputs.Add(p_c_output_label, 'Caption');
EvaluateOutputs; End; // with g_c_binding_expression
End; // set_outputs_and_evaluate | - the main program contains a tMemo for the source String. Our example string is:
source "boss "+ the_person.FirstName + " "+ the_person.LastName
+ " cy "+ the_company.Company | and several buttons to call the creation of the objects, the compilation, the evaluation, and several display methods which will be presented now.
3.2 - Creating an identifier / value environment 3.2.1 - What's in RootScope ? Our main purpose is to be able to explain and display the different objects involved into a simple expressions LiveBindings.
We quickly run into an Interface problem. Many properties were defined as Interfaces but to display anything, we have to find out what the implementing object is in order to investigate its content.
For instance, a tBindingExpressionDefault has a RootScope, which is defined as an iScope :
IScope = Interface
Function Lookup(Const Name: String): IInterface;
End; TBindingExpressionDefault = Class(TBindingExpression, ...
Protected
Property RootScope: IScope read FRootScope implements IScope;
... End; | So, basically, a scope is an environment which can be looked up. But what's in
this environment and how is it organized ? Unless we know a little bit more about the object implementing iScope in this case, we cannot display too much.
So we had to invest into Rtti, until we managed to display the RootScope.
3.3 - tDictionaryScope To build the environment, the compiler needs to initialize ("register") basic litterals and operators.
This is performed by MakeBasicConstants() and MakeBasicOperators() in EVALSYS. Basically MakeBasicConstants creates a tValueWrapper around the value True (or the value False, Nil or Pi).
A tValueWrapper wraps litteral values, and can be used to fetch the type and the value. It is defined by
TValueWrapper = Class(TInterfacedObject, IWrapper, IValue)
Private FValue: TValue;
Public
Constructor Create(Const AValue: TValue);
{ IValue }
Function GetValue: TValue;
Function GetType: PTypeInfo; End; |
with the following Interfaces : IWrapper =
Interface End; IValue = Interface
//Used to obtain the type information for the actual value
Function GetType: PTypeInfo;
Function GetValue: TValue; End; |
Here is how to create a tValueWrapper and display the value
Var l_c_TRUE_value_wrapper: tValueWrapper;
l_c_TRUE_value_wrapper:= TValueWrapper.Create(true);
display(l_c_TRUE_value_wrapper.GetValue.ToString); |
Those litteral values are grouped into dictionaries, namely a tDictionaryScope which are used to lookup the litteral name, say 'True' to get the value wrapper which returns True. The tDictionaryScope is defined by
TDictionaryScope =
Class(TInterfacedObject, IScope, IScopeEx, IScopeEnumerable)
Public Type
TMap = TDictionary<String, IInterface>;
Constructor Create;
Property Map: TMap read FMap;
{ IScope }
Function Lookup(Const Name: String): IInterface; overload;
... End; |
We can add our "True" wrapper with this code:
Var l_c_dictionary_scope: TDictionaryScope;
l_c_dictionary_scope := TDictionaryScope.Create;
l_c_dictionary_scope.Map.Add('True', l_c_TRUE_value_wrapper); |
and we can lookup the 'True' string using:
Var l_c_lookup_value_wrapper: tValueWrapper;
l_c_lookup_value_wrapper:= l_c_dictionary_scope.Lookup('True') As tValueWrapper;
display(l_c_lookup_value_wrapper.GetValue.ToString); |
After those first trials, we will now start our expression LiveBinding
3.4 - Step 1 : Creating the tBindingAssociation 3.4.1 - tBindingAssociation
We first stores the relation between a Delphi object and a expression object using a tBindingAssociation Record :
TBindingAssociation = Record Public
RealObject: TObject;
ScriptObject: String;
Constructor Create(ARealObject: TObject; Const AScriptObject: String);
End; |
Here is our code:
Var l_c_person_binding_association: tBindingAssociation; l_c_person_binding_association:=
tBindingAssociation.Create(g_c_person, 'the_person'); |
and we can obviously redisplay this association Record
With l_c_person_binding_association Do Begin
display('real_object '+ RealObject.ClassName);
display('real_object.ToString '+ RealObject.ToString);
display('ScriptObject: '+ ScriptObject+ '<'); End; |
3.5 - Step 2 : Creating and compiling the tBindingExpression 3.5.1 - tBindingExpression The workhorse of the LiveBindings is the tBindingExpression. This Class contains
- a dictionary of the binding associations
- a scope for the outputs
- Compile and evaluateOutputs methods
The Class is defined as:
TBindingExpression =
Class Abstract(TObject) Public
Type
TAssociationPair = TPair<TObject, String>;
TAssociations = TDictionary<TObject, String>;
Procedure Compile(Const Assocs: Array Of TBindingAssociation); overload;
Procedure EvaluateOutputs; Virtual; Abstract;
Property Outputs: TBindingOutput read FBindingOutput;
Property OutputValue: TValue read GetOutputValue write SetOutputValue;
... End; | where - Compile is used to add the array of tBindingAssociations
- Outputs is used to specify where the result of the evaluation will be stored
- EvaluateOutputs will start the evaluation
And, for our simple code LiveBinding, we used a simple tBindingExpression
descendent : TBindingExpressionDefault =
Class(TBindingExpression,
IScope, IScopeEx, IScopeEnumerator, IScopeSymbols,
ICompiledBinding, ICompiledBindingWrappers) Private
FBinding: ICompiledBinding;
FRootScope: IScope; Protected
Property RootScope: IScope read FRootScope implements IScope;
... End; | and: - RootScope will hold the environment (the links between the string
identifiers and the token values, the wrappers)
- fBinding is the parsed expressions
3.5.2 - Creation and compilation of the binding expression Here is the code:
Var g_c_binding_expression: tBindingExpressionDefault= Nil;
g_c_binding_expression:= c_my_binding_expression_default.Create;
g_c_binding_expression.Source:= p_source;
g_c_binding_expression.Compile([ l_c_person_binding_association,
l_c_company_binding_association ]); |
3.5.3 - Displaying the environment scopes
To display the tBindingExpression we neet to know the type of the RootScope implementing object. First we must make this Protected RootScope visible. We simply define a
tBindingExpression descendent with RootScope promoted to Public visibility
Type c_my_binding_expression_default=
Class(tBindingExpressionDefault) Public
Property RootScope; End;
Var l_c_root_scope: iScope; l_c_root_scope:=
c_my_binding_expression_default(g_c_binding_expression).RootScope; |
Then we simply convert this RootScope iScope Interface to the implementing object, using any of the Interface to tObject technique :
display(f_c_interface_to_object(l_c_root_scope).ClassName); |
3.5.4 - tNestedScope In our case, the RootScope is implemented by a tNestedScope object
TNestedScope =
Class(TInterfacedObject, IInterface, IScope, IScopeEx,
IScopeEnumerable, IScopeSelf) Public
Property Inner: IScope read FInner;
Property Outer: IScope read FOuter;
Function Lookup(Const Name: String): IInterface; overload;
... End; | This tNestedScope is used to build the nested environment. Like in Pascal, in
a Procedure you can have a Var which is a Record containing an Array etc. The Lookup procedure simply is :
Function TNestedScope.Lookup(Const Name: String): IInterface;
Begin Result := Inner.Lookup(Name);
If Result = Nil Then
Result := Outer.Lookup(Name);
End; | And this clearly demonstrates what a "Scope" really is.
To display the Inner iScope, we used the same Interface to Object technique,
to display the object implementing this Interface. It turns out to be a tDictionaryScope here. So we created a procedure which can display those objects.
The tDictionaryScope contains a tDictionary <String, iInterface>. To analyze the items of a tDictionary, we iterate thru the Keys and use the key value to get the Values. Something like this:
Var l_string_key: String;
l_i_value: iInterface; l_c_value: tObject;
With p_c_dictionary_scope Do
With Map Do
For l_string_key In Keys Do
Begin
l_i_value:= Items[l_string_key];
l_c_value:= f_c_interface_to_object(l_i_value);
display(' wrapper.ClassName '+ l_c_value.ClassName);
End; |
The Interface to Object tells us that the iInterface is implemented by a tObjectWrapper Class:
TObjectWrapper = Class(TInterfacedObject,
IWrapper, IValue, ILocation,
IPlaceholder, IWrapperBinding,
IScope, IScopeEx, IScopeSelf, IScopeEnumerable, IScopeSymbols)
... End; |
So we simply have to display this tObjectWrapper. Simple ? Not quite. THIS requires some explainin:
The tObjectWrapper is hidden inside the Implementation part of the System.Bindings.ObjEval Unit. To display this kind of object we can - either create a local copy of the definition and use this definition to cast
the iInterface
- or query the iInterface for iValue or iScope (using Supports) to extract the information
We used the second technique, and here is our display procedure
Procedure display_dictionary_scope(p_c_dictionary_scope: tDictionaryScope);
Var l_string_key: String;
l_i_value: iInterface; l_c_value: tObject;
l_i_the_value: iValue;
l_i_the_value_value: tValue; Begin
With p_c_dictionary_scope Do Begin
// -- analyze the tDictionary<String, iInterface>
With Map Do
For l_string_key In Keys Do
Begin
display('Key '+ l_string_key);
l_i_value:= Items[l_string_key];
l_c_value:= f_c_interface_to_object(l_i_value);
display(' wrapper.ClassName '+ l_c_value.ClassName);
// -- get the iValue Interface
If Supports(l_i_value, iValue, l_i_the_value)
Then Begin
display(' supports ivalue');
display(' type : '+ l_i_the_value.GetType.Name);
l_i_the_value_value:= l_i_the_value.GetValue;
display(' value.type '+ l_i_the_value_value.TypeInfo.Name);
If l_i_the_value_value.IsObject
Then Begin
display(' isObject');
display(' the_value_is '
+ l_i_the_value_value.AsObject.ToString);
End;
End; End;
End; // with p_c_dictionary_scope
End; // display_dictionary_scope
Procedure display_nested_scope(p_c_nested_scope: tNestedScope);
Var l_c_dictionary_scope: TDictionaryScope; Begin
With p_c_nested_scope Do Begin
display('p_c_nested_scope.Inner type_name: '
+ f_c_interface_to_object(Inner).ClassName);
If Inner Is tDictionaryScope
Then Begin
l_c_dictionary_scope:= TDictionaryScope(Inner);
display('Inner');
display_dictionary_scope(l_c_dictionary_scope);
End; End;
End; // display_nested_scope
display_nested_scope(l_c_root_scope As tNestedScope); |
with the following result (with both g_c_person and g_c_company associations added to the expression) :
3.5.5 - The compiling process steps When we call my_binding_expression.Compile:
3.6 - The compiled expression: iCompiledBinding
tBindingExpressionDefault.fBinding stores the compiled expression. However fBinding is a Private member. So we had to use a cast to reach this member:
Type c_bogus_binding_expression=
Class(TBindingExpression) Private
FBinding: ICompiledBinding;
Public
Function f_i_binding : ICompiledBinding;
End;
Function c_bogus_binding_expression.f_i_binding: ICompiledBinding;
Begin Result:= fBinding;
End; // f_i_binding
Var l_i_compiled_binding: iCompiledBinding; l_i_compiled_binding:=
c_bogus_binding_expression(g_c_binding_expression).f_i_binding; |
The fBinding is defined as an iCompiledBinding Interface, which will only be used to call Evaluate for computing the result of an evaluation :
ICompiledBinding = Interface
Function Evaluate(ARoot: IScope;
ASubscriptionCallback: TSubscriptionNotification;
{out} Subscriptions: TList<ISubscription>): IValue;
End; | In fact the BindExpression.Compile transforms the string source expression in
a pseudo code array (list of elementary operations on the environment), stores this code in the implementing Class, and uses this pseudo code to evaluate different environments when EvaluateOutputs is called.
In our case, the fBinding is implemented by a tCompiledBinding Class : TCompiledBinding =
Class(TInterfacedObject, ICompiledBinding, ICompiledBindingWrappers,
IDebugBinding) ... End; |
Since this Class is, again, nested in the Implementation of the System.Bindings.Evaluator Unit, we cannot directly analyze it. We decided to call the tempting iDebugBinding Interface, defined as.
IDebugBinding = Interface
Procedure Dump(Const W: TProc<String>);
End; | Therefore the dump uses the classic Anonymous Method technique to add debug tracing. Therefore we wrote the following code:
Procedure display_compiled_binding(p_i_compiled_binding: iCompiledBinding);
Var l_c_object: tObject;
l_i_debug_binding: iDebugBinding; Begin
l_c_object:= f_c_interface_to_object(p_i_compiled_binding);
display('comp_bind '+ l_c_object.ClassName);
If Supports(p_i_compiled_binding, IDebugBinding, l_i_debug_binding)
Then Begin
l_i_debug_binding.Dump(
Procedure(value: String)
Begin
display(value);
End );
End; End;
End; // display_compiled_binding | And here is the result:
Please note
3.7 - Step 3 : Initializing the Outputs Once the environment has been setup and the pseudo-code computed, we now tell what our output should be:
g_c_binding_expression.Outputs.Add(Label1, 'Caption'); |
tBindingOutput is defined by TBindingOutput = Class
Public Type
TOutputPair = TPair<TObject, String>;
TDestinations = TDictionary<ILocation, TOutputPair>;
Private FOutputs: TDestinations;
Public
Procedure Add(AObject: TObject; Const PropertyName: String); overload;
Property Destinations: TDestinations read FOutputs;
End; | and: - Add receives theLabel1 tObject and its property name, Caption
- the result is stored into Destinations
We can display the output information:
Procedure display_outputs(p_c_output: TBindingOutput);
Var l_i_location: iLocation;
l_c_output_pair: TBindingOutput.tOutputPair;
l_value: tValue; l_c_object: tObject;
Begin With p_c_output Do
Begin // display(IntToStr(Destinations.Count));
For l_i_location In Destinations.Keys Do
Begin
l_c_output_pair:= Destinations.Items[l_i_location];
l_value:= l_c_output_pair.Value;
display('output_pair_property '+ l_value.ToString);
l_c_object:= l_c_output_pair.Key As tObject;
If l_c_object= Nil
Then display('l_c_object_NIL')
Else display('output_pair_Object.ClassName '+ l_c_object.ClassName+ '<');
End; End; // with p_c_output
End; // display_outputs |
3.8 - Step 4 : Evaluating the expression Finally we call the expression evaluation :
g_c_binding_expression.EvaluateOutputs; |
This method
Procedure TBindingExpressionDefault.EvaluateOutputs; Begin
SetOutputs( Function: IValue
Begin
Result := fBinding.Evaluate(FRootScope, Nil, Nil);
End );
End; // EvaluateOutputs | where - the Anonymous method calls the fBinding (which is an iCompiledBinding)
Evaluate method, which returns an Ivalue
- this iValue is handed over to SetOutputs which uses its Outputs property to propagate the value to the Label1.Caption
3.9 - The LiveBinding UML Class Diagram Finally we could draw the following UML Class Diagram :
Where - in blue everything which is concerned with storing the environment (the identifiers along with their types and input values)
- in green the main part: tBindingExpression, and the simplified
tBindingExpressionDefault
- the tBindingAssociation is just a temporary helper class used to input the string <-> object relation into the environment
- tBindingOutput contains the target items of the compiler
- iCompiledBinding (or tCompiledBinding) contains the pseudo code after the compilation and uses an Evaluate method to transfer the result of the evaluation into the tBindingExpression.Outputs
4 - Comments 4.1 - Interface To Object To convert any Interface to the tObject which implements this Interface,
we used a method f_interface_to_object which is in the U_INTERFACE_TO_OBJECT unit The technique is around since 2001. I saw it the first time in the Delphi 6 beta news groups. I then wrote the Dump Interface
article, back in April 2004. Several other versions were then published. Hallvard Vassbotn even wrote an article in the Delphi Magazine, in October 2004
Our unit contains the Barry KELLY version, which was an answer to a Stack Overflow question and later changed into a blog post. The reason we used this version is that I did not know (and still have not checked)
whether the Delphi 6 (2001) hack was still valid in 2011.
Of course, when we are sure of the type of the implementing object, we can use AS or even directly cast the Interface with the implementing object (which
was used in the code above)
4.2 - LiveBinding Compiler in perspective After all this activity, il all boils down to - the BindingExpression builds an environment of all the identifier, storing
along with the string the type and value information of each identifier
- this environment consists of
- the predefined litterals and operators
- the identifiers extracted by Compile from the source string expression
- Compile also builds an "executable" which is a pseudo code array
- once the Outputs have been specified, the EvaluateOutputs runs the stack machine interpreter to build the result and send it to the outputs. The only
difference is that the result can be sent to outputs specified AFTER the compilation. You do not compile an assignment, but you compile an expression, and the resulting value can be sent to any compatible output.
LiveBindings are not a full fledged compiler. Their sole purpose is to link objects together using String Expression.
In this article, we took a VERY low level viewpoint, with a big risk of looking at the tree and not the forest
- first of all it uses the tBindingExpressionDefault, which is a simplified tBindingExpression for evaluations by code
- our example did not investigate all the callbacks and notifiers we saw while looking at the sources
- some functionalities could not be implemented, or we did not understand how to use them. For instance, we could not convert an Integer Property. There surely is are ToStr or Format functions, and our
LiveBinding Tutorial did use them (as do some of the SourceForge samples). But we had no success in our tBindingExpressionDefault.
- analyzing the example tells us what kind of data can be involved in Delphi LiveBindings. In our case tObject Properties to Component properties. Keep in mind however that our example uses the special
tBindingExpressionDefault, and we only presented the data and methods involved in THIS example. There are many other properties and Overloaded methods of the involved Classes that we did not present here.
With the previous understanding under our belt, it would be interesting to now try a top down approach, starting for instance from a simple tBindExpression, to try to bridge the gaps. Understanding tBindingScope, how the notification
works, what Managers are, exploring the wrappers, adding numeric conversions ...
4.3 - The coding Style Understanding this code forced us to actually work with Rtti, Generics,
Anonymous Methods, and this was certainly long overdue. From my (old-hand Apple ][ Pascal coder) point of view, the code is a little bit high on the "coding to the Interface" side.
Defining RootScope as an iScope seems somehow an overkill. Like Sender: tObject for an tWinControl notifications. This certainly allows to use nearly any type of object which can be looked up
as a RootScope. And no doubt, this is all the compiler needs to do: just call Lookup on RootScope. However this makes it more difficult to dump the different objets involved.
Another nostalgic comment would be sophistication of the code. Niklaus WIRTH's Pascal P4 Compiler is about 3500 lines for the compiler and 1500 lines for the interpreter. The LiveBinding sources are around 800 K bytes (granted, bytes, not LOC).
However using the last available technology (Rtti, Generics, Anonymous Methods), LiveBinding allow us to have runtime compilation and late binding.
4.4 - The current documentation
No doubt the whole Embarcadero team was busy as hell during those last months. This perhaps explains the little amount of help about LiveBindings. The presentations are good, and the tutorials well explained. But the details about
each class is somehow lacking. In fact, it only takes 10 minutes to see that the current Wiki (and HLP) documentation is a simple reformatting of the /// and <summary> comments
present in the source code. So reading the sources will tell you MUCH more about the details of LiveBindings than looking at the documentation. You have the signatures, the adjacent informations and concepts, the grouping in
Units, the Structure Treeview. I can only URGE you to spend an hour or so browsing this code. For the part we covered here, the Units are - System.Rtti
- System.Generics.Collections
- tPair
- tDictionary
- Proc < String>
- System.Bindings.EvalProtocol
- iWrapper
- iValue
- iLocation
- iScope
- iCompiledBinding
- tValueWrapper
- System.Bindings.EvalSys
- TDictionaryScope
- MakeBasicConstants ()
- MakeBasicOperators ()
- TNestedScope
- System.Bindings.CustomWrapper
- System.Bindings.Outputs
- System.Bindings.Evaluator
- compile ()
- tCompiledBinding
- Evaluate ()
- System.Bindings.ObjEval
- System.Bindings.Expression
- tBindingAssociation
- tBindingExpression
- System.Bindings.ExpressionDefaults
- tBindingExpressionDefault
In fact, to have those nearby I copied them in the project directory (some times the Delphi XE2 was sick and tired of my casting mistakes and "Find In Files" refused to work, and on the other hand the Delphi 6 IDE "Find in Files"
stalled on the "System.xxx" names in the Uses clause). So directly loading the units from a file explorer was a shortcut.
5 - Download the Sources Here are the source code files: The .ZIP file(s) contain:
- the main program (.DPR, .DOF, .RES), the main form (.PAS, .DFM), and any other auxiliary form
- any .TXT for parameters, samples, test data
- all units (.PAS) for units
Those .ZIP
- are self-contained: you will not need any other product (unless expressly mentioned).
- for Delphi 6 projects, can be used from any folder (the pathes are RELATIVE)
- will not modify your PC in any way beyond the path where you placed the .ZIP (no registry changes, no path creation etc).
To use the .ZIP: - create or select any folder of your choice
- unzip the downloaded file
- using Delphi, compile and execute
To remove the .ZIP simply delete the folder. The Pascal code uses the Alsacian notation, which prefixes identifier by
program area: K_onstant, T_ype, G_lobal, L_ocal, P_arametre, F_unction, C_lass etc. This notation is presented in the Alsacian Notation paper. The .ZIP file(s) contain:
- the main program (.DPROJ, .DPR, .RES), the main form (.PAS, .ASPX), and any other auxiliary form or files
- any .TXT for parameters, samples, test data
- all units (.PAS .ASPX and other) for units
Those .ZIP
- are self-contained: you will not need any other product (unless expressly mentioned).
- will not modify your PC in any way beyond the path where you placed the .ZIP
(no registry changes, no path outside from the container path creation etc).
To use the .ZIP: - create or select any folder of your choice.
- unzip the downloaded file
- using Delphi, compile and execute
To remove the .ZIP simply delete the folder. The Pascal code uses the Alsacian notation, which prefixes identifier by program area: K_onstant, T_ype, G_lobal, L_ocal, P_arametre,
F_unction, C_lass etc. This notation is presented in the Alsacian Notation paper.
As usual:
- please tell us at fcolibri@felix-colibri.com if you found some errors, mistakes, bugs, broken links or had some problem downloading the file. Resulting corrections will
be helpful for other readers
- we welcome any comment, criticism, enhancement, other sources or reference suggestion. Just send an e-mail to fcolibri@felix-colibri.com.
- or more simply, enter your (anonymous or with your e-mail if you want an answer) comments below and clic the "send" button
- and if you liked this article, talk about this site to your fellow developpers, add a link to your links page ou mention our articles in
your blog or newsgroup posts when relevant. That's the way we operate: the more traffic and Google references we get, the more articles we will write.
6 - References - Interface to Object
- for Rtti
- LiveBindings in RAD Studio
the Delphi WIKI contains presentations about LiveBindings - the Delphi XE2 SourceForge repository contains about 10 LiveBinding samples, covering both Vcl and FireMonkey examples, for simple controls and for database LiveBindings
- In the core of LiveBindings expressions of RAD Studio XE2
Daniele TETI - Aug 30 2011
the basic tBindingExpression example, presented just before the Delphi XE2 launch - Delphi XE2
LiveBindings Tutorial
John Colibri - 30 Sept 2011 - 54K 6 sample codes, 43 figs - how to setup the SourceComponent and the ControlComponent and expression, tBindingsList, the bindings Editor, using several sources
with tBindingScope, building bindings by code, LiveBindings and databases. Far more flexible than the Vcl db_xxx, but with the risks of late binding (in French)
- FireMonkey Architecture : the basic tComponent <- tFmxObject <- Fmx.tControl <- tStyledControl hierarchy.
Firemonkey UML Class diagram, and short feature description.
- Simple FireMonkey Object Inspector
Felix COLIBRI - 10 Oct 2011 - 52 K, 2 .ZIP source, 5 Fig building a FireMonkey Object Inspector which presents the components of
the Form and displays their property names an values and allows the user to modify them at runtime - FireMonkey Style Explorer : create
tFmxObjects from their class name, create their default style, display their child style herarchy in a tTreeView, present each style element in an Object Inspector which can be used to change the property values.
- The Pascal P4 Compiler
Niklaus WIRTH - 1976 Where it all started ...
7 - The author
Felix John COLIBRI works at the Pascal Institute. Starting with Pascal in 1979, he then became involved with Object Oriented Programming, Delphi, Sql, Tcp/Ip, Html, UML. Currently, he is mainly
active in the area of custom software development (new projects, maintenance, audits, BDE migration, Delphi
Xe_n migrations, refactoring), Delphi Consulting and Delph
training. His web site features tutorials, technical papers about programming with full downloadable source code, and the description and calendar of forthcoming Delphi, FireBird, Tcp/IP, Web Services, OOP / UML, Design Patterns, Unit Testing training sessions. |